﻿using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Media.Animation;
using Black.SurfaceToolkit.Tools;
using Microsoft.Surface.Presentation.Controls;

namespace Black.SurfaceToolkit.Behaviors
{
    /// <summary>
    /// Gère le comportement de Fling Out des éléments d'un <see cref="T:Microsoft.Surface.Presentation.Controls.ScatterView" />.
    /// </summary>
    public static class ScatterViewFlingOutBehavior
    {
        #region Evénements statiques

        /// <summary>
        /// Se produit lorsqu'un Fling Out se termine.
        /// </summary>
        public static event RoutedEventHandler FlingOutEnd;

        /// <summary>
        /// Se produit lorsqu'un Fling Out commence.
        /// </summary>
        public static event RoutedEventHandler FlingOutStart;

        #endregion

        #region Propriétés de dépendance

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="P:Black.SurfaceToolkit.Behaviors.ScatterViewFlingOutBehavior.IsFlingOut" />.
        /// </summary>
        public static readonly DependencyProperty IsFlingOutProperty = DependencyProperty.RegisterAttached("IsFlingOut", typeof(bool), typeof(UIElement), new UIPropertyMetadata(BooleanBoxes.FalseBox, ScatterViewFlingOutBehavior.OnIsFlingOutChanged));

        #endregion

        #region Accesseurs statiques

        /// <summary>
        /// Obtient une valeur indiquant si le Fling Out est activé sur l'élément spécifié.
        /// </summary>
        /// <param name="p_oElement">Elément dont on veut savoir si le Fling Out est activé.</param>
        /// <returns>Valeur indiquant si le Fling Out est activé sur <paramref name="p_oElement" />.</returns>
        /// <exception cref="T:System.ArgumentNullException"><paramref name="p_oElement" /> est <see langword="null" />.</exception>
        [AttachedPropertyBrowsableForType(typeof(ScatterView))]
        [AttachedPropertyBrowsableForType(typeof(ScatterViewItem))]
        public static bool GetIsFlingOut(UIElement p_oElement)
        {
            if (p_oElement == null)
                throw new ArgumentNullException("p_oElement");

            return ((bool)p_oElement.GetValue(ScatterViewFlingOutBehavior.IsFlingOutProperty));
        }

        /// <summary>
        /// Définit une valeur indiquant si le Fling Out est activé sur l'élément spécifié.
        /// </summary>
        /// <param name="p_oElement">Elément pour lequel on souhaite activer ou désactiver le Fling Out.</param>
        /// <param name="p_oValue">Valeur indiquant si le Fling Out est activé.</param>
        /// <exception cref="T:System.ArgumentNullException"><paramref name="p_oElement" /> est <see langword="null" />.</exception>
        [AttachedPropertyBrowsableForType(typeof(ScatterView))]
        [AttachedPropertyBrowsableForType(typeof(ScatterViewItem))]
        public static void SetIsFlingOut(UIElement p_oElement, bool p_oValue)
        {
            if (p_oElement == null)
                throw new ArgumentNullException("p_oElement");

            p_oElement.SetValue(ScatterViewFlingOutBehavior.IsFlingOutProperty, BooleanBoxes.Box(p_oValue));
        }

        #endregion

        #region Fonctions statiques privées

        /// <summary>
        /// Crée et démarre le <see cref="T:System.Windows.Media.Animation.Storyboard" /> pour le Fling Out de l'élément spécifié.
        /// </summary>
        /// <param name="p_oElement">Elément à animer.</param>
        /// <param name="p_oGoTo">Point de destination.</param>
        private static void CreateAndStartFlingOutStoryboard(UIElement p_oElement, Point p_oGoTo)
        {
            // On supprime la gestion du Fling Out

            ScatterViewFlingOutBehavior.RemoveTracking(p_oElement);

            // Event de lancement

            if (ScatterViewFlingOutBehavior.FlingOutStart != null)
                ScatterViewFlingOutBehavior.FlingOutStart(p_oElement, new RoutedEventArgs());

            // Si pas un FrameworkElement on termine tout de suite

            if (!(p_oElement is FrameworkElement))
                ScatterViewFlingOutBehavior.FinishStoryboardComplete(p_oElement, EventArgs.Empty);

            // Cast de l'élément

            FrameworkElement oFElement = p_oElement as FrameworkElement;

            // Création du Stroyboard

            Storyboard oStoryboard = new Storyboard();

            DoubleAnimation oAnimationAlpha = new DoubleAnimation(0, new Duration(new TimeSpan(0, 0, 0, 0, 500)));
            PointAnimation oAnimationCenter = new PointAnimation(p_oGoTo, new Duration(new TimeSpan(0, 0, 0, 0, 500)));

            Storyboard.SetTarget(oAnimationAlpha, oFElement);
            Storyboard.SetTarget(oAnimationCenter, oFElement);

            Storyboard.SetTargetProperty(oAnimationAlpha, new PropertyPath(UIElement.OpacityProperty));
            Storyboard.SetTargetProperty(oAnimationCenter, new PropertyPath(ScatterViewItem.CenterProperty));

            oStoryboard.Children.Add(oAnimationAlpha);
            oStoryboard.Children.Add(oAnimationCenter);

            oStoryboard.Completed += new EventHandler(ScatterViewFlingOutBehavior.FinishStoryboardComplete);

            // Démarage du storyboard

            oFElement.BeginStoryboard(oStoryboard);
        }

        /// <summary>
        /// Appelée sur à la fin du Fling Out.
        /// </summary>
        /// <param name="p_oSender">Source de l'événement.</param>
        /// <param name="p_oArgs">Données de l'événement.</param>
        private static void FinishStoryboardComplete(object p_oSender, EventArgs p_oArgs)
        {
            ScatterViewItem oItem = null;

            // Récupération de la vraie target

            p_oSender = Storyboard.GetTarget(((p_oSender as ClockGroup).Timeline as Storyboard).Children[0]);

            // On retire l'objet si m_dC'est un ScatterViewItem

            if (ScatterViewFlingOutBehavior.IsScatterViewItem(p_oSender, out oItem))
            {
                // Récupération du ScatterView

                ScatterView oScatter = ScatterViewFlingOutBehavior.GetScatterView(oItem);

                // On retire l'objet

                oScatter.Items.Remove(oItem);
            }

            // Event de fin


            if (ScatterViewFlingOutBehavior.FlingOutEnd != null)
                ScatterViewFlingOutBehavior.FlingOutEnd(p_oSender, new RoutedEventArgs());
        }

        /// <summary>
        /// Obtient le <see cref="Microsoft.Surface.Presentation.Controls.ScatterView" /> de l'élément spécifié.
        /// </summary>
        /// <param name="p_oElement">Elément dont on veut le <see cref="Microsoft.Surface.Presentation.Controls.ScatterView" />.</param>
        /// <returns><see cref="Microsoft.Surface.Presentation.Controls.ScatterView" /> de <paramref name="p_oElement" />, <see langword="null" /> sinon.</returns>
        private static ScatterView GetScatterView(object p_oElement)
        {
            // Test de l'élément direct

            if (p_oElement is ScatterView)
                return (p_oElement as ScatterView);

            // Test si m_dC'est un ScatterViewItem pour récupérer son ScatterView de ratachement

            ScatterViewItem oScatterViewItem = null;

            if (ScatterViewFlingOutBehavior.IsScatterViewItem(p_oElement, out oScatterViewItem))
                return (oScatterViewItem.Parent as ScatterView);

            // Pas de ScatterView

            return (null);
        }

        /// <summary>
        /// Obtient le <see cref="Microsoft.Surface.Presentation.Controls.ScatterViewItem" /> de l'élément spécifié.
        /// </summary>
        /// <param name="p_oElement">Elément dont on veut le <see cref="Microsoft.Surface.Presentation.Controls.ScatterViewItem" />.</param>
        /// <returns><see cref="Microsoft.Surface.Presentation.Controls.ScatterViewItem" /> de <paramref name="p_oElement" />, <see langword="null" /> sinon</returns>
        private static ScatterViewItem GetScatterViewItem(object p_oElement)
        {
            // Test de l'élément direct

            if (p_oElement is ScatterViewItem)
                return (p_oElement as ScatterViewItem);

            // Test si m_dC'est un ScatterViewItem pour récupérer son ScatterView de ratachement

            if (p_oElement is FrameworkElement && (p_oElement as FrameworkElement).Parent is ScatterViewItem)
                return ((p_oElement as FrameworkElement).Parent as ScatterViewItem);

            // Pas de ScatterView

            return (null);
        }

        /// <summary>
        /// Obtient une valeur indiquant si l'élément spécifié est un <see cref="Microsoft.Surface.Presentation.Controls.ScatterViewItem" />.
        /// </summary>
        /// <param name="p_oElement">Elément dont on veut savoir s'il est un <see cref="Microsoft.Surface.Presentation.Controls.ScatterViewItem" />.</param>
        /// <returns><see langword="true" /> si <paramref name="p_oElement" /> est un <see cref="Microsoft.Surface.Presentation.Controls.ScatterViewItem" />, <see langword="false" /> sinon.</returns>
        private static bool IsScatterViewItem(object p_oElement)
        {
            // Test de l'élément directement et dans son parent

            if (p_oElement is ScatterViewItem)
                return (true);
            else if (p_oElement is FrameworkElement && (p_oElement as FrameworkElement).Parent is ScatterViewItem)
                return (true);
            else if (p_oElement is Storyboard)
                return (false);

            // N'est pas un ScatterViewItem

            return (false);
        }

        /// <summary>
        /// Obtient une valeur indiquant si l'élément spécifié est un <see cref="Microsoft.Surface.Presentation.Controls.ScatterViewItem" />.
        /// </summary>
        /// <param name="p_oElement">Elément dont on veut savoir s'il est un <see cref="Microsoft.Surface.Presentation.Controls.ScatterViewItem" />.</param>
        /// <param name="p_oItem"><see cref="Microsoft.Surface.Presentation.Controls.ScatterViewItem" /> associé, <see langword="null" /> si <paramref name="p_oElement" /> n'est pas un <see cref="Microsoft.Surface.Presentation.Controls.ScatterViewItem" />.</param>
        /// <returns><see langword="true" /> si <paramref name="p_oElement" /> est un <see cref="Microsoft.Surface.Presentation.Controls.ScatterViewItem" />, <see langword="false" /> sinon.</returns>
        private static bool IsScatterViewItem(object p_oElement, out ScatterViewItem p_oItem)
        {
            // Par defaut pas de retour

            p_oItem = null;

            // Test de l'élément directement et dans son parent

            if (p_oElement is ScatterViewItem)
            {
                p_oItem = p_oElement as ScatterViewItem;
                return (true);
            }
            else if (p_oElement is FrameworkElement && (p_oElement as FrameworkElement).Parent is ScatterViewItem)
            {
                p_oItem = (p_oElement as FrameworkElement).Parent as ScatterViewItem;
                return (true);
            }

            // N'est pas un ScatterViewItem

            return (false);
        }

        /// <summary>
        /// Appelée lorsque la propriété <see cref="P:Black.SurfaceToolkit.Behaviors.ScatterViewFlingOutBehavior.IsFlingOut" /> est modifiée.
        /// </summary>
        /// <param name="p_oSender">Source de l'événement.</param>
        /// <param name="p_oArgs">Données de l'événement.</param>
        private static void OnIsFlingOutChanged(DependencyObject p_oSender, DependencyPropertyChangedEventArgs p_oArgs)
        {
            ScatterViewItem oItem = null;

            // Si m_dC'est un ScatterViewItem on s'abonne au Delta pour trouver la sortie

            if (ScatterViewFlingOutBehavior.IsScatterViewItem(p_oSender, out oItem))
            {
                if ((bool)p_oArgs.NewValue)
                    oItem.ScatterManipulationDelta += new ScatterManipulationDeltaEventHandler(ScatterViewFlingOutBehavior.ScatterManipulationDelta);
                else
                    oItem.ScatterManipulationDelta -= new ScatterManipulationDeltaEventHandler(ScatterViewFlingOutBehavior.ScatterManipulationDelta);
            }
            else if (p_oSender is ScatterView)
            {
                if ((bool)p_oArgs.NewValue)
                    ((p_oSender as ScatterView).Items as INotifyCollectionChanged).CollectionChanged += new NotifyCollectionChangedEventHandler(ScatterViewFlingOutBehavior.ScatterCollectionChanged);
                else
                    ((p_oSender as ScatterView).Items as INotifyCollectionChanged).CollectionChanged -= new NotifyCollectionChangedEventHandler(ScatterViewFlingOutBehavior.ScatterCollectionChanged);
            }
        }

        /// <summary>
        /// Supprime le suivi du Fling Out.
        /// </summary>
        /// <param name="p_oElement">Elément dont on veut supprimer le suivi du Fling Out.</param>
        private static void RemoveTracking(UIElement p_oElement)
        {
            ScatterViewItem oItem = null;

            // Si ScatterViewItem on retire le tracking

            if (ScatterViewFlingOutBehavior.IsScatterViewItem(p_oElement, out oItem))
                oItem.ScatterManipulationDelta -= new ScatterManipulationDeltaEventHandler(ScatterViewFlingOutBehavior.ScatterManipulationDelta);
            else if (p_oElement is ScatterView)
                ((p_oElement as ScatterView).Items as INotifyCollectionChanged).CollectionChanged -= new NotifyCollectionChangedEventHandler(ScatterViewFlingOutBehavior.ScatterCollectionChanged);
        }

        /// <summary>
        /// Appelée lorsque la collection des éléments du <see cref="T:Microsoft.Surface.Presentation.Controls.ScatterView" /> spécifié change.
        /// </summary>
        /// <param name="p_oSender">Source de l'événement.</param>
        /// <param name="p_oArgs">Données de l'événement.</param>
        private static void ScatterCollectionChanged(object p_oSender, NotifyCollectionChangedEventArgs p_oArgs)
        {
            if (p_oArgs.NewItems != null)
            {
                foreach (ScatterViewItem oItem in p_oArgs.NewItems)
                    ScatterViewFlingOutBehavior.SetIsFlingOut(oItem, true);
            }
        }

        /// <summary>
        /// Appelée lorsque la manipulation du <see cref="T:Microsoft.Surface.Presentation.Controls.ScatterView" /> spécifié évolue.
        /// </summary>
        /// <param name="p_oSender">Source de l'événement.</param>
        /// <param name="p_oArgs">Données de l'événement.</param>
        private static void ScatterManipulationDelta(object p_oSender, ScatterManipulationDeltaEventArgs p_oArgs)
        {
            ScatterViewItem oItem = ScatterViewFlingOutBehavior.GetScatterViewItem(p_oSender as UIElement);
            ScatterView oScatter = ScatterViewFlingOutBehavior.GetScatterView(oItem);

            // Test qu'on a bien récupérer les éléments pour le fling out

            if (oItem == null || oScatter == null)
                return;

            // On vérifie qu'aucun contact n'est présent

            if (oItem.ContactsCapturedWithin.Count == 0 && oItem.IsAnyContactCaptured == false)
            {
                double dTailleRectif = (oItem.ActualWidth > oItem.ActualHeight ? oItem.ActualHeight : oItem.ActualWidth) / 2;

                // Test sortie de l'élément

                if (oItem.ActualCenter.X < dTailleRectif)
                {
                    oItem.CancelManipulation();
                    ScatterViewFlingOutBehavior.CreateAndStartFlingOutStoryboard(p_oSender as UIElement, new Point(oItem.Center.X - 200, oItem.Center.Y));
                }
                else if (oItem.ActualCenter.X > (oScatter.ActualWidth - dTailleRectif))
                {
                    oItem.CancelManipulation();
                    ScatterViewFlingOutBehavior.CreateAndStartFlingOutStoryboard(p_oSender as UIElement, new Point(oItem.Center.X + 200, oItem.Center.Y));
                }
                else if (oItem.ActualCenter.Y < dTailleRectif)
                {
                    oItem.CancelManipulation();
                    ScatterViewFlingOutBehavior.CreateAndStartFlingOutStoryboard(p_oSender as UIElement, new Point(oItem.Center.X, oItem.Center.Y - 200));
                }
                else if (oItem.ActualCenter.Y > (oScatter.ActualHeight - dTailleRectif))
                {
                    oItem.CancelManipulation();
                    ScatterViewFlingOutBehavior.CreateAndStartFlingOutStoryboard(p_oSender as UIElement, new Point(oItem.Center.X, oItem.Center.Y + 200));
                }
            }
        }

        #endregion
    }
}
